/*
*  ssl.c - SSL management functions
*  Copyright (c) 2005 Derek Ingrouville (dergro@users.sourceforge.net)
*
*  This program is free software; you can redistribute it and/or modify
*  it under the terms of the GNU General Public License as published by
*  the Free Software Foundation; either version 2 of the License, or
*  (at your option) any later version.
*
*  This program is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*  GNU Library General Public License for more details.
*
*  You should have received a copy of the GNU General Public License
*  along with this program; if not, write to the Free Software
*  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

#include <glib.h>
#include <string.h>

#include "ssl.h"

#ifdef HAVE_OPENSSL
/* OpenSSL specific functions */

SSLSocket *
ssl_connect (const char *addr, int port)
{
    SSLSocket *sslsock = NULL;
    Socket *sock = NULL;
    SSL_METHOD *ssl_method;

    g_return_val_if_fail (addr != NULL, NULL);
    g_return_val_if_fail (strlen (addr) > 3, NULL);
    g_return_val_if_fail (port > 0, NULL);

    sock = socket_new ();
    socket_connect (sock, addr, port, FALSE);
    
    if (!sock->connected) {
        socket_close (sock);
        return NULL;
    }

    sslsock = g_new (SSLSocket, 1);
    sslsock->socket = sock;

    SSL_library_init ();
    ssl_method = SSLv23_client_method ();

    sslsock->ctx = SSL_CTX_new (ssl_method);
    if (sslsock->ctx == NULL) {
        ssl_close (sslsock);
        return NULL;
    }

    sslsock->ssl = SSL_new (sslsock->ctx);
    if (sslsock->ssl == NULL) {
        ssl_close (sslsock);
        return NULL;
    }

    SSL_set_fd (sslsock->ssl, sock->sock_fd);
    if (SSL_connect (sslsock->ssl) <= 0) {
        ssl_close (sslsock);
        return NULL;
    }

    sslsock->connected = TRUE;
    return sslsock;
}

void
ssl_close (SSLSocket *sslsock)
{
    g_return_if_fail (sslsock != NULL);

    if (sslsock->ctx != NULL)
        SSL_CTX_free (sslsock->ctx);
       
    if (sslsock->ssl != NULL)
        SSL_free (sslsock->ssl);
    
    if (sslsock->socket->connected)
        socket_close (&*sslsock->socket);

    g_free (sslsock);

    return;
}

int
ssl_read (SSLSocket *sslsock, char *buffer, unsigned int size)
{
    int total = 0;

    g_return_val_if_fail (sslsock != NULL, -1);
    g_return_val_if_fail (sslsock->connected, -1);
    g_return_val_if_fail (sslsock->ssl != NULL, -1);
    g_return_val_if_fail (buffer != NULL, -1);
    g_return_val_if_fail (size > 0, -1);

    while (ssl_data_available (sslsock) && (total < size)) {
        int ret = 0;
        ret = SSL_read (sslsock->ssl, buffer + total, size - total);
        if (ret <= 0)
            break;
        total += ret;
    }

    return total;
}

int
ssl_write (SSLSocket *sslsock, const char *buffer, int len)
{
    int sent = 0;

    g_return_val_if_fail (sslsock != NULL, 0);
    g_return_val_if_fail (sslsock->ssl != NULL, 0);
    g_return_val_if_fail (buffer != NULL, 0);
    g_return_val_if_fail (len > 0, 0);

    while (sent < len) {
		int ret = 0;

        ret = SSL_write (sslsock->ssl, buffer, len);

		if (ret < 0)
			break;

		sent += ret;
    }

    return sent;
}

#elif HAVE_GNUTLS
/* GNUTLS specific functions */

int ssl_connect (SSLSocket *sslsock, const char *addr, int port)
{
    static const int cert_type_priority[2] = { GNUTLS_CRT_X509, 0 };
    int ret = 0;

    g_return_val_if_fail (*sslsock != NULL, -1);
    g_return_val_if_fail (addr != NULL, -1);
    g_return_val_if_fail (strlen (addr) > 3, -1);
    g_return_val_if_fail (port > 0, -1);

    if (socket_connect (&*sslsock->socket, addr, port, FALSE) != 0)
        return -1;

    if (!*sslsock->socket->connected) {
        ssl_close (*sslsock);
        return -1;
    }

    gnutls_global_init ();
    gnutls_certificate_allocate_credentials (&(*sslsock->xcred));
    gnutls_certificate_set_x509_trust_file (*sslsock->xcred, "ca.pem", GNUTLS_X509_FMT_PEM);

    gnutls_init (&(*sslsock->session), GNUTLS_CLIENT);
    gnutls_set_default_priority (*sslsock->session);
    gnutls_certificate_type_set_priority (*sslsock->session, cert_type_priority);
    gnutls_credentials_set (*sslsock->session, GNUTLS_CRD_CERTIFICATE, *sslsock->xcred);
    gnutls_transport_set_ptr (*sslsock->session, GINT_TO_POINTER (*sslsock->socket->sock_fd));

    do
        ret = gnutls_handshake (*sslsock->session);
    while ((ret == GNUTLS_E_AGAIN) || (ret == GNUTLS_E_INTERRUPTED));

    if (ret != 0) {
        ssl_close (*sslsock);
        return -1;
    }

    return 0;
}

int ssl_read (SSLSocket *sslsock, char *buffer, unsigned int size)
{
    int total = 0;

    g_return_val_if_fail (*sslsock != NULL, -1);
    g_return_val_if_fail (*sslsock->connected, -1);
    g_return_val_if_fail (*sslsock->session != NULL, -1);
    g_return_val_if_fail (buffer != NULL, -1);
    g_return_val_if_fail (size > 0, -1);

    while (ssl_data_available (*sslsock) && total < size) {
        int     ret = 0;
        ret = gnutls_record_recv (*sslsock->session, buffer + total, size - total);
        if (ret <= 0)
            break;
        total += ret;
    }

    return total;
}

int ssl_write (SSLSocket *sslsock, const char *buffer, int len)
{
    int sent = 0;

    g_return_val_if_fail (*sslsock != NULL, 0);
    g_return_val_if_fail (*sslsock->session != NULL, 0);
    g_return_val_if_fail (buffer != NULL, 0);
    g_return_val_if_fail (len > 0, 0);

    while (sent < len) {
		int ret = 0;

        ret = gnutls_record_send (*sslsock->session, buffer, len);

		if (ret < 0)
			break;

		sent += ret;
    }

    return sent;
}

void ssl_close (SSLSocket *sslsock)
{
    g_return_if_fail (*sslsock != NULL);

    if (*sslsock->session != NULL) {
        gnutls_bye (*sslsock->session, GNUTLS_SHUT_RDWR);
        gnutls_deinit (*sslsock->session);
        *sslsock->session = NULL;
    }

    if (*sslsock->xcred != NULL) {
        gnutls_certificate_free_credentials (*sslsock->xcred);
        *sslsock->xcred = NULL;
    }

    gnutls_global_deinit ();

    if (*sslsock->socket->connected)
        socket_close (&*sslsock->socket);

    *sslsock->connected = 0;

    return;
}
#endif /* HAVE_GNUTLS */

char *ssl_recv_all (SSLSocket *sslsock)
{
    const unsigned int min_size = 4096;
    size_t  data_size = 0;
    int     ret = 0;
    char   *recv_buffer = NULL;
    char   *data_buffer = NULL;

    g_return_val_if_fail (sslsock != NULL, NULL);
    g_return_val_if_fail (sslsock->connected, NULL);

    recv_buffer = g_new0 (char, min_size);

    while ((ret = ssl_read (sslsock, recv_buffer + data_size, min_size)) > 0) {
        data_size += ret;
        recv_buffer = g_realloc (recv_buffer, (data_size + min_size));
    }

    if (ret <= 0 && data_size == 0) {
        g_free (recv_buffer);
        recv_buffer = NULL;
        return NULL;
    }

    data_buffer = g_new0 (char, data_size + 1);
    g_strlcpy (data_buffer, recv_buffer, data_size);

    g_free (recv_buffer);
    recv_buffer = NULL;

    data_buffer[data_size] = '\0';
    return data_buffer;
}

gboolean ssl_data_available (SSLSocket *sslsock)
{
    g_return_val_if_fail (sslsock != NULL, FALSE);
    g_return_val_if_fail (sslsock->connected, FALSE);
    g_return_val_if_fail (sslsock->socket->connected, FALSE);

    return socket_data_available (sslsock->socket);
}

